The ultimate matplotlib tutorial (1)

🔖 python
🔖 visualization
Author

Guangyao Zhao

Published

Oct 10, 2022

Abstract
第一次知道 ultimate 这个词是接触 Ulysses app 时,其口号是 The ultimate writing App for Mac, iPad and iPhone。 正如『终究写作 App』一般,我想从数据可视化更高的立意上梳理清一个画布的主要元素,以求达到科技写作出版级别质量的图形。在此我想要写出一篇我心目中的 The ultimate matplotlib tutorial

1 Overview

在开始之前先声明一下本文宗旨:

  • 本文目标是画布以及子图框架的搭建,并不涉及具体图形,比如散点图,条形图等。
  • 画图就像堆积木,一点点『添加』最终成型。所以在 matplotlib 中,我推荐使用 addset 的写法。一点点添加元素,直到最终输出图形。
  • 本文一个图形对应一段完整的代码(除了导入第三方库)。
  • 初学者请先坚决抛弃 pyplot 交互模块(explicit interfaces),或许会有些不便,但对 matplotlib 整体框架的把握有非常大的好处。
  • 本文之所以不得不使用 pyplot 是因为要输出图形,但在 Sec. 8 中给出了不使用 pyplot 的写法。

Matplotlib 中有 124 个 模块, 604 个类,4899 个方法,840 个属性。我们不可能全部将其掌握,只能从更高的层次上整体把握其思想。

在 matplotlib 中图形的最高层次是 figure(先不管 Artist 这种复杂的概念), 所有的元素都在 figure 中叠加。figure 上可以在不同的位置添加不同的子图(subplot),然后子图中有坐标轴(xaxis, yaxis),坐标轴上有轴标签(xlabel, ylabel)、刻度线(xtick, ytick)和刻度线标签(xticklabelsyticklabels),别忘了还有图例(legend)。最后给子图分配合适的位置,整个 figure 就算是搭起来了。

接下来要做的是对以上元素进行细致的设置,每一个元素都有非常多细致的设置,掌握一些常用的命令,我认为足够做出出版级别(publication-quality)的图,当然了,这也是官方文档的说法。

在开始之前先导入要用的程序包,在此再啰嗦一句,初学者请抛弃那种便利但混杂的plt写法:

import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np

2 Figure

想象我们作图时需要什么?最重要的当然是作图的载体,也就是纸。在 matplotlib 作图时同样如此,只不过在此大家习惯称之为画布(figure),画布本身又含有许多元素,比如整个画布的大小(figsize);还有你是选择白纸还是黑纸,对应到 matplotlib 中便是背景色(facecolor)了;画布有边框(frameon)吧,边框是线,线有自己的固有属性,比如颜色(edgecolor)和线宽(linewidth),至此画布的物理状态已经被确定了。

fig = plt.figure(figsize=(8, 6),
                 facecolor='pink',
                 frameon=True,
                 edgecolor='green',
                 linewidth=2)

画布设置好后,就可以画图了,matplotlib 中一张画布可以有多张子图(add_subplot),既然有多张子图,就会涉及到子图的定位和排列问题,官方文档 也给出了很多示例。在介绍多子图之前,先介绍单图的情况,即一张画布只有一张图形:

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")

x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)

ax1.plot(x, y)
plt.show()

Fig. 1: Add_subplot

2.1 General settings

子图框架设置好以后,便可对其标题(title)、边框(frameon)、背景(facecolor)、坐标轴标签(xlabel, ylabel)、坐标轴范围(xlim, ylim, xmargin, ymargin)、刻度线(xticks, yticks)、刻度线标签(xticklabels, yticklabels)、网格(grid)、图例(legend)做出细致的设置了。设置方法分为两种:

  • 直接在add_subplot()时设置。
  • 采用set_title()之类的方法设置。

前者更直接方便,但不可设置其属性,只能使用默认值,后者反之。

在子图的设置中,标题、边框、背景、坐标轴标签、坐标轴范围、网格的设置相对比较简单,下面一起说明。刻度线、图例则之后分别说明。

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

x = np.linspace(-10, 10, 100)
y = np.sin(x)
z = np.cos(x)

ax1 = fig.add_subplot(
    111,
    title="Subplot1",
    frameon=True,  # 坐标轴边框
    facecolor="white",  # 背景色
    xlabel="$x$",  # 坐标轴标签
    ylabel="$y$",
    xlim=(-10, 10),  # 坐标轴范围
    ylim=(-1, 1),
    xticks=[-10, -6, -2, 2, 6, 10],
    xticklabels=["-ten", "-six", "-two", "two", "six", "ten"],
    xmargin=0.1,  # 自动根据数据范围扩充坐标轴范围,会覆盖 xlim
    ymargin=0.1,
)

ax1.grid(which="both", color="green", linestyle="--", linewidth=0.5)

ax1.plot(x, y, color="red")
ax1.plot(x, z, color="green")
plt.show()

Fig. 2: Setting

Fig. 2 中虽然给出了grid的简单示例,但一般不推荐大家使用该元素。

2.2 Tick

我觉得 tick 是整个图形中最复杂的部分,它包括刻度线样式和刻度线标签字体样式(tick_params)的定义之外,还包括对刻度线的定位(locator)和刻度线标签的显示(formatter)两个重要的部分。

此处列出地已基本足够科技论文出图的元素,更细致的设置请参考官方文档

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")

# 对主刻度线进行设置
ax1.tick_params(
    axis="both",  # 同时对横纵轴设置
    which="major",  # 设置针对主刻度
    direction="inout",  # 刻度线方向
    length=6,  # 长度
    width=2,  # 宽度
    color="red",  # 刻度线颜色
    labelsize=18,  # 刻度标签字体大小
    pad=20,
)

ax1_xmajor, ax1_ymajor = 4, 0.5
ax1_xmajor_locator = mpl.ticker.MultipleLocator(ax1_xmajor)  # 设置主刻度间隔
ax1_ymajor_locator = mpl.ticker.MultipleLocator(ax1_ymajor)
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
ax1.yaxis.set_major_locator(ax1_ymajor_locator)

# 对副刻度线进行设置
ax1.tick_params(
    axis="both", which="minor", direction="in", length=4, width=1, color="black"
)
ax1_xminor_locator = mpl.ticker.MultipleLocator(ax1_xmajor / 2)  # 设置副刻度间隔
ax1_yminor_locator = mpl.ticker.MultipleLocator(ax1_ymajor / 2)
ax1.xaxis.set_minor_locator(ax1_xminor_locator)
ax1.yaxis.set_minor_locator(ax1_yminor_locator)

x = np.linspace(-10, 10, 500)
y = np.sin(x)
z = np.cos(x)

ax1.plot(x, y)
ax1.plot(x, z)
plt.show()

Fig. 3: Tick

Locator

控制刻度线位置的元素叫locator,也就是定位器。大部分情况下,默认的定位器是无法满足需求的,要想更细致地调节刻度显示,就需要重写 set_major_locator() 方法。这部分是我觉得在整个 matplotlib 中的一个难点,所以在此尽可能写的详细一些。

一般根据默认设置,就可以得到一个还不错的刻度线显示,但是如果要更加精细地对齐控制,就需要阅读一些源码了。作为示例,看一下 IndexLocator(Locator) 的源码:

class IndexLocator(Locator):
    """
    Place a tick on every multiple of some base number of points
    plotted, e.g., on every 5th point.  It is assumed that you are doing
    index plotting; i.e., the axis is 0, len(data).  This is mainly
    useful for x ticks.
    """
    def __init__(self, base, offset):
        """Place ticks every *base* data point, starting at *offset*."""
        self._base = base
        self.offset = offset

    def set_params(self, base=None, offset=None):
        """Set parameters within this locator"""
        if base is not None:
            self._base = base
        if offset is not None:
            self.offset = offset

    def __call__(self):
        """Return the locations of the ticks"""
        dmin, dmax = self.axis.get_data_interval()
        return self.tick_values(dmin, dmax) # Log at WARNING level if *locs* is longer than `Locator.MAXTICKS`.

    def tick_values(self, vmin, vmax):
        return self.raise_if_exceeds(
            np.arange(vmin + self.offset, vmax + 1, self._base))

方法:

  • set_params(self, base=None, offset=None): 获取起始位置和偏移。
  • __call__(self): 如果超过刻度线太多,抛出异常提示。
  • tick_values(self, vmin, vmax): 返回刻度线列表。

其中重点看一下np.arange(vmin + self.offset, vmax + 1, self._base),这一句的意思就是根据提供的起始值,按照步长扩展到整个刻度线列表。

接下来演示一下其它几种定位器。

NullLocator

不显示刻度线:

ax1_xmajor_locator = mpl.ticker.NullLocator()
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
FixedLocator

显示给定的刻度线:

li_locator = [0, 1, 2]
ax1_xmajor_locator = mpl.ticker.FixedLocator(li_locator) # 仅显示 0, 1, 2
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
AutoLocator

[1, 2, 2.5, 5, 10] 以及其倍数中自动寻找合适的步长:

ax1_xmajor_locator = mpl.ticker.AutoLocator() # 自动寻找合适的步长
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
MultipleLocator

以给定步长生成刻度线:

ax1_xmajor = 2 # 步长为 2
ax1_xmajor_locator = mpl.ticker.MultipleLocator(ax1_xmajor)
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
LinearLocator

MultipleLocator 相反,LinearLocator 是根据给定的刻度线数自动算步长:

ax1_numticks = 10 # 显示 10 个刻度线
ax1_xmajor_locator = mpl.ticker.LinearLocator(ax1_numticks)
ax1.xaxis.set_major_locator(ax1_xmajor_locator)
LogLocator

当图形和对数有关时就需要用到 LogLocator 了:

ax1.set_xscale('log')  # 在使用 LogLocator 之前需要声明轴比例
ax1_xmajor_locator = mpl.ticker.LogLocator(
    base=10,  # 以 10 为底,可自定义
    subs=(1.0, ), #TODO
    numdecs=4,  # 不可自定义副刻度个数,LogLocator已经写死了
    numticks=10)  # 最多可以出现的刻度标签
ax1.xaxis.set_major_locator(ax1_xmajor_locator)

Formatter

定位器选好后,就可以在特定的刻度线上显示特定的刻度线标签了,这也是非常重要的一部分内容。

NullFormatter

NullFormatter() 直接将刻度线标签设置为空,但是刻度线依旧显示:

ax1_xmajor_locator = mpl.ticker.NullFormatter()  # 设置刻度线标签为空
ax1.xaxis.set_major_formatter(ax1_xmajor_locator)
FixedFormatter

FixedFormatter(seq) 需要和 FixedLocator() 搭配使用,一切都是自己指定:

li_locator = [-8, 8]
ax1_xmajor_locator = mpl.ticker.FixedLocator(li_locator)  # 设置主刻度间隔
ax1.xaxis.set_major_locator(ax1_xmajor_locator)

str_formatter = ['-eight', 'eight']
ax1_xmajor_formatter = mpl.ticker.FixedFormatter(str_formatter)  # 设置主刻度间隔
ax1.xaxis.set_major_formatter(ax1_xmajor_formatter)
FormatStrFormatter

FormatStrFormatter(fmt) 是一个比较有意思的功能,可以在原刻度线显示的基础上添加特定的前缀或后缀,用 %d 表示原刻度标签,比如在此基础上给它一个漂亮国的到乐前缀:

str_formatter = mpl.ticker.FormatStrFormatter('\$ %d')
ax1.xaxis.set_major_formatter(str_formatter)
PercentFormatter

PercentFormatter(xmax, decimals, symbol, is_latex)xmax 为显示的最大值,decimals为显示的精度,symbol为百分比号的显示形式,is_latex是否为 LaTex 格式。事实上将百分比号换成其它符号也可以。

per_formatter = mpl.ticker.PercentFormatter(xmax=100,
                                            decimals=1,
                                            symbol='\%',
                                            is_latex=True)
ax1.xaxis.set_major_formatter(per_formatter)
LogFormatter

LogFormatter(base, labelOnlyBase, minor_thresholds, linthresh)base 是对数底数

log_formatter = mpl.ticker.LogFormatter(base=10)
ax1.xaxis.set_major_formatter(log_formatter)
FuncFormatter

2.3 Legend

legend 的设置十分丰富,必要时建议查阅官方文档进行细致地调控。在此仅列出一些重要参数,特别要注意的是,legend 要放在 label 后面,否则无法正常显示(其原因也不难理解,知道标签后才能给出图例):

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$", ylim=(-1.5, 2.5))

x = np.linspace(-10, 10, 500)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x) + np.cos(x)
y4 = np.sin(x) - np.cos(x)

ax1.plot(x, y1, label="$\sin(x)$", color="red")
ax1.plot(x, y2, label="$\cos(x)$", color="green")
ax1.plot(x, y3, label="$\sin(x)+\cos(x)$", color="blue")
ax1.plot(x, y4, label="$\sin(x)-\cos(x)$", color="orange")

ax1.legend(
    loc="upper right",  # 位置
    frameon=True,  # 边框
    edgecolor="black",  # 边框颜色
    borderpad=2,  # 边框和图例的距离
    ncol=2,  # 一行有几列
)
plt.show()

Fig. 4: Legend

2.4 Spines

Spines 的中文翻译是轴脊线,它控制着子图的边框设置,比如边框的显示(visuable),颜色(color),线宽(linewidth)等,更多的设置参考官方文档

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")

ax1.spines["left"].set_color("red")  # 左轴颜色为红色
ax1.spines["left"].set_linewidth(5)  # 左轴线宽为红色
ax1.spines["right"].set_visible(False)  # 设置右轴不可见
ax1.spines["top"].set_visible(False)

x = np.linspace(-10, 10, 500)
y = np.sin(x)

ax1.plot(x, y)

Fig. 5: Spines

实际上坐标轴只能设置为不可见,即 set_visible(False),不可以实际删除。

3 Subfigure

以上在单图之外,我们往往需要的是整齐排列的子图,(2, 2) 排列也好 (2, 3) 排列也好,总之想要得到的是规整的子图排列。假如想要的是 4 个子图,排列为 (2, 2),此时就可以使用 add_subplot(22i) 直接添加,其中 i 是按横向数第 i 个子图,matplotlib 会很贴心地按照 4 等分将子图自动定位:

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")

x = np.linspace(0, 10, 100)

ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
plt.show()

Fig. 6: Subfigure

3.1 Subplots_adjust

Fig. 6 中可看出默认子图间横纵都有间隔,既然有间隔,那就可以调,在 matplotlib 中用 subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=None) 调节子图边框(不包含刻度线、刻度线标签和坐标轴标签)整个画布的关系,基准点为画布的左下。比如:

  • left = 0.1:子图区域的最左边框和画布左边框的间距为画布宽度\(0.1\)
  • right = 1:子图区域的最右边框和画布右边框重合。
  • bottom = 0.1: 子图区域的最下边框和画布最下边框的间距为画布高度\(0.1\)
  • wspace = 0.25:子图间的横向间距为子图平均宽度\(0.25\)
  • hspace = 0.25:子图间的纵向间距为子图平均高度\(0.25\)
fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

# 设置画布整体布局,以边框为边界
fig.subplots_adjust(left=0.1, right=1, bottom=0.1, top=1, wspace=0.25, hspace=0.25)

ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")

x = np.linspace(0, 10, 100)

ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
plt.show()
# fig.savefig(fname='figure.pdf')

Fig. 7: Subplots_adjust

Warning

right=1, top=1 表示的是子图右边框和上边框之外完全无空白,但是 Fig. 7 却没有正确显示出来,这可能是交互模式下的显示问题,如果使用 savefig() 命令则可正确显示。

3.2 Sharex and sharey

在多子图的情况下,可能存在若干子图的坐标轴设置一样,正如官方文档中描述的一样 Share the x or y axis with sharex and/or sharey. The axis will have the same limits, ticks, and scale as the axis of the shared axes.,可以共享已有的设置:

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

# The axis will have the same limits, ticks, and scale as the axis of the shared axes.
ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$", xlim=(0, 100), ylim=(-2, 2))
ax2 = fig.add_subplot(
    222, sharex=ax1, xlabel="$x$", ylabel="$y$"
)  # 和 ax1 共享 x 轴,但 y 轴并不一样

ax3 = fig.add_subplot(223, xlim=(0, 50), ylim=(-2, 2), xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(
    224, xlabel="$x$", ylabel="$y$", sharex=ax3, sharey=ax3
)  # 和 ax3 共享 x,y 轴

x = np.linspace(0, 10, 100)

ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
plt.show()

Fig. 8: Sharexy

3.3 Gridspec

至此,我们基本上已经清楚地了解规则多子图的画法,但有时我们的子图排列并不总是规则的,此时就要用到 matplotlib 的 add_gridspec(),从字面上便可理解,它是给画布添加一个大格子,大格子包含了(n, m)个小格子,根据小格子索引分配各个子图的位置和大小:

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

gs = fig.add_gridspec(2, 2)  # 大格子分为 2*2 的小格子

ax0 = fig.add_subplot(gs[0, :], xlabel="$x$", ylabel="$y$")  # 格子索引从 0 开始,`0:` 表示整行
ax10 = fig.add_subplot(gs[1, 0], xlabel="$x$", ylabel="$y$")  # 第 2 行的第 1 列
ax11 = fig.add_subplot(gs[1, 1], xlabel="$x$", ylabel="$y$")

Fig. 9: Add_gridspec

Tip

在子图的排列不规则时,可以先在纸上画出所有的小格子,然后严格按照小格子确定子图的位置和大小。

4 Text and annotate

有时候会特别需要一些文字标注,对图形加以解释。此时有两种标注方法:

  • text():仅有文本,无箭头。除文本外需要提供文本锚点。
  • annotate():有文本和箭头指向。不仅需要提供文本(text)和文本锚点(textxy),还需要给出箭头的终止位置(xy)。
fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")

sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-((x - mu) ** 2)) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)

ax1.text(x=-2, y=0.07, s="Text: This is a Gaussian distribution")  # text 标注
ax1.annotate(
    xytext=(-2, 0.025),  # 文本位置
    text="Annotate: This is a Gaussian distribution",
    xy=(-4, 0.025),  # 箭头终点
    arrowprops=dict(arrowstyle="->"),
)  # 含有箭头的标注

ax1.plot(x, y)
plt.show()

Fig. 10: Text and annotate

5 Axhline and axvspan

有时也需要对图形划出一条线,以作为某种参考。比如想标注出 \(x \ge 0.025\)\(y \ge 0.025\)),以清楚地了解哪些 \(x\) 落入该范围,此时就要用到 axhline()axvline();同样的有时候想标注的不仅是条线,而是一片区域,此时就要用到 axhspan()axvspan()

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")

sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-((x - mu) ** 2)) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)

ax1.axhline(
    y=0.025,  # 横线的高度,即 y 值
    xmin=0.1,  # 横线从 x 轴的哪里开始,比例
    xmax=0.9,  # 横线从 x 轴的哪里结束,比例
    color="red",
    linestyle="-",
)
ax1.axvline(x=-2)  # 纵向

ax1.axvspan(
    xmin=-2,  # 填充起始位置
    xmax=2,  # 填充终止位置
    ymin=0.025,  # 填充从 x 轴哪里开始,比例
    ymax=0.05,
    facecolor="green",
)

ax1.plot(x, y)
plt.show()

Fig. 11: Line and span

6 LaTex

在 matplotlib 中也对 LaTex 进行了完整的支持,这也意味着可以直接调用 LaTex 丰富的宏包进行高质量输出,比如常用的 \usepackage{siunix} 进行单位输出,在设置之前请先确认下电脑是否已经安装 LaTex。下面介绍一下其用法:

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(
    111,
    xlabel="$x$",
    ylabel="$y$",
    title="{\\ttfamily This is a Gaussian distribution (monospaced font).}",
)

sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-((x - mu) ** 2)) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)

ax1.text(x=-2, y=0.07, s="siunix: $\SI{10}{g/L}$")  # 测试宏包是否可用
ax1.text(x=-2, y=0.05, s="mhchem: $\ce{C + O2 -> CO2}$")
ax1.text(
    x=-2,
    y=0.03,
    s="$f(x)=\\frac{1}{\sigma \sqrt{2 \pi}} e^{-\\frac{(x-\mu)^{2}}{2 \sigma^{2}}}$",
)

ax1.plot(x, y)
plt.show()
ValueError: 
\SI{10}{g/L}
^
Unknown symbol: \SI, found '\'  (at char 0), (line:1, col:1)
<Figure size 768x576 with 1 Axes>

Fig. 12: LaTex

使用如下命令获取 matplotlib 配置文件路径:

import matplotlib as mpl
path = mpl.matplotlib_fname()

获取 matplotlibrc 配置文件路径后可对其进行设置,但我觉得尽量还是不要对其修改。下面我给出一个 LaTex 的修改例子:

text.usetex        : True  # 支持 LaTex 输出
text.latex.preamble: \usepackage{amsmath}\usepackage{amssymb}\usepackage{mhchem}\usepackage{siunitx}
mathtext.fontset: cm # 数学字体格式,好像只有为数不多的字体可供选择。['dejavusans', 'dejavuserif', 'cm', 'stix', 'stixsans', 'custom']
Warning

特别要注意的是导言区(preamble)的配置语法,直接使用 \usepackage{},中间不需要任何分隔符。另外在 LaTex 模式下,坐标轴标签(其它根据默认设置显示正常)默认是非衬线字体,至少目前看来是这样的。所以需要手动调,以下给出一个例子:

fontdict = dict(family='Arial') # 设置字体
xticks = ax1.get_xticks() # 拿到坐标轴
ax1.set_xticklabels(labels=xticks, fontdict=fontdict) # 设置坐标轴

7 Savefig

至此,除了具体的图形展示(条形图,散点图,频率直方图等),你已经清楚了从创建画布,到整个图形的框架设置的完整流程。接下来只需保存即可,在此可以对图形框架进行很多设置,但大多都和之前的设置重复。还是建议严格的按照以上的设置流程,规范作图步骤。另外,科技写作图形推荐矢量图格式 .pdf,具体矢量图和位图的区别可自行学习。

如果前面每一步都设置好了,图片的保存就显得十分简单:

fig.savefig(fname='figure.pdf')
fig.savefig(fname='figure', format='png', dpi=300)

8 Say no to pyplot

Pyplot 模块给 matplotlib 提供了交互模式,为 matplotlib 的学习提供了便利,但同时又给初学者带来了无尽的困扰。

它来回穿插在 matplotlib 的各个类和方法之间,使代码看起来杂乱无章。所以我建议初学者暂且拒绝 pyplot,踏踏实实一步步创建类,调用方法来完成图形,下面给出一个完全抛弃 pyplot 的作图流程:

fig = Figure(figsize=(8, 6),
             facecolor='pink',
             frameon=True,
             edgecolor='green',
             linewidth=2)
ax1 = fig.add_subplot(111, xlabel='$x$', ylabel='$y$')

# 设置刻度线
ax1_xmajor = 1
ax1_xmajor_locator = mpl.ticker.MultipleLocator(
    ax1_xmajor)  # 直接调用 MultipleLocator 类
ax1.xaxis.set_major_locator(ax1_xmajor_locator)

sigma = 2
mu = 0
x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 150)
y = np.exp((-(x - mu)**2) / (2 * (sigma**2))) / (np.sqrt(2 * np.pi) * sigma)

ax1.plot(
    x,
    y,
    label=
    '$f(x)=\\frac{1}{\sigma \sqrt{2 \pi}} e^{-\\frac{(x-\mu)^{2}}{2 \sigma^{2}}}$'
)
ax1.legend(loc='best')

fig.savefig('Gaussian distribution.pdf')

9 Elements

Matplotlib 中有许多独立的元素可以进行精细的设置,以下列举出常用的元素和该元素常用的设置。

9.1 Marker

在绘制散点图的时候,有个参数为marker,具体可参考官方文档

Marker可设置的属性有:

  • s:大小
  • c:颜色
  • marker:种类
  • linewidth:线宽(边缘线)
  • edgecolor:边缘线颜色
  • alpha:透明度
fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)
ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")

np.random.seed(19680801)
N = 100  # 数据个数
r0 = 0.6  # 分类界限
x = 0.9 * np.random.rand(N)
y = 0.9 * np.random.rand(N)
area = (20 * np.random.rand(N)) ** 2  # 0 to 10 point radii
c = np.sqrt(area)  # 颜色
r = np.sqrt(x**2 + y**2)
area1 = np.ma.masked_where(r < r0, area)  # 小于 area
area2 = np.ma.masked_where(r >= r0, area)  # 大于等于 area

ax1.scatter(x, y, s=area1, marker="^", c=c)  # 大小  # marker  # 颜色
ax1.scatter(x, y, s=area2, marker="o", c=c)
# Show the boundary between the regions:
theta = np.arange(0, np.pi / 2, 0.01)
ax1.plot(r0 * np.cos(theta), r0 * np.sin(theta))
plt.show()

Fig. 13: Marker

9.2 Line and arrow

线的形状一般有:

  • -
  • --
  • -.
  • :
  • custom

除了以上,还可以根据自己的喜好自定义样式

还可以设置不同的虚线结合方式,具体参考set_dash_capstyle(s)

  • set_dashes([6, 2]):6pt line, 2pt break.
  • set_dashes([2, 2, 10, 2]):2pt line, 2pt break, 10pt line, 2pt break.

Line 可设置的属性有:

  • linestyle:形状
  • linewidth:宽度
  • color:颜色
fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(111, xlabel="$x$", ylabel="$y$")

x = np.linspace(0, 10, 100)

ax1.plot(x, np.sin(x), linestyle="--")
ax1.plot(x, np.cos(x), linestyle="-.")
ax1.plot(x, np.sin(x) + 0.1, linestyle=":")
ax1.plot(x, np.cos(x) + 0.1, dashes=[2, 2, 10, 2])
plt.show()

Fig. 14: Line

Arrow 和 Line 相比只是多加了一个箭头,箭头样式参考官方文档,其余用法和 Line 类似。

9.3 Font

在包括 LaTex 的排版中,字体分为正文字体和数学字体,这两个可以分别设置。在 matplotlib 中同样如此。 相对于LaTex, 虽然 matplotlib 的字体设置更加丰富(参考官方文档),但我还是更推荐学习下 LaTex 的字体内容。

在介绍具体地操作之前,先整理下字体的种类都有哪些(列举一些我喜欢的)。

正文字体

字体族

  • 衬线字体(Serif):Bitter,Palatino,New York,Times
  • 非衬线字体(Sans-serif):Arial,Avenir,Seravek
  • 等宽字体(Monospace):Source Code Pro,Monaco

字体形状

  • 直立(Upright shape)
  • 意大利(Italic shape)
  • 倾斜(Slanted shape)
  • 小型大写(Small captials shape)

字体系列

  • 中等(Medium series)
  • 加宽加粗(Bold extended series)

数学字体

数学字体一般使用 Times,这部分的介绍在刘海洋的 LaTex 入门中写的比较详细,请自行阅读,事实上也推荐系统地学习下这本书。

直接给出 LaTex 命令:

  • \mathrm{}:衬线
  • \mathsf{}:非衬线
  • \mathtt{}:等宽
  • \mathit{}:变量,数学模式下默认该斜体
  • \mathbb{}:空心字体,比如实数域 \(\mathbb{R}\)
  • \mathcal{}:常用于具体运算符,比如损失函数 \(\mathcal{L}\)

Matplotlib 的可用字体

在设置字体之前,先看一下哪些字体可用:

from matplotlib import font_manager

for font in font_manager.fontManager.ttflist:
    # 查看字体名以及对应的字体文件名
    print(font.name, '-', font.fname)

设置中文字体

在默认情况下是不支持中文字体的,此时需要根据各自电脑中安装的字体手动设置:

plt.rcParams['font.family'] = ['sans-serif'] # 设置正文字体族为 sans-serif,此处可自行设置
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 根据字体族,设置中文字体

当然了,任何字体都可以按照以上方式自行设置。比如:

plt.rcParams['font.family'] = ['serif'] # 设置正文字体族为 sans-serif
plt.rcParams['font.sans-serif'] = ['Times', 'Palatino'] # 衬线
plt.rcParams['font.serif'] = ['Arial', 'Helvetica'] # 非衬线
plt.rcParams['font.monospace'] = ['Source Code Pro', 'Monaco'] # 等宽
plt.rcParams['font.size']=10 # 字体大小

以上是非 LaTex 环境的字体设置,LaTex 的字体设置请参考 Sec. 6 部分。

各元素字体配置

在 matplotlib 中,字体的设置可以打包成一个字典,但凡需要设置字体的元素直接添加 dict() 即可,要用到字体设置的元素一般有(可根据以下方式命名,方便管理)以下这些,更多具体的设置请参考官方文档

fontproperties

  • 坐标轴标签(xlabel_font):ax1.set_xlabel(), ax1.set_ylabel()
  • 子图标题(subplot_font):ax1.set_title()
  • 带箭头指向的标注(annotate_font):ax1.annoatate()

prop

  • 图例(legend_font):ax1.legend()

fontdict

  • 刻度线标签(xtick_font):ax1.set_xticklabels(), ax1.set_yticklabels()
  • 标注(text_font):ax1.text()
Danger

字体的设置本身不复杂,但是根据以上层级,首先各自参数名不同,其次也来自不同的类,比较容易混淆。其中 fontpropertiespropFontProperties实例,fontdictText实例,因其参数名称可能不同,具体设置请谨慎查阅官方文档。

text_font = {
    'family': 'Source Code Pro', # 可指定字体族,如 sans-serif, serif, monospace, 也可直接指定可用字体
    'fontsize': 12, # 大小
    'color': 'green', # 字体颜色
    'rotation': 'horizontal', # 旋转角度,也可 vertical 和具体角度
    'bbox': { # 文字盒子设置
        'boxstyle': 'round',
        'facecolor': 'pink',
        'edgecolor': 'black',
        'linewidth': 1 
    }
}

ax1.text(x=2, y=0, s='Test, Test', fontdict=text_font) # 非数学公式表达
ax1.text(x=2, y=1, s='$xy,\\mathrm{xy},\\mathbb{R},\\mathcal{L}$', fontdict=text_font) # 数学表达

9.4 Color

Matplotlib 支持的颜色格式有如下几种:

  • RGB(0.1, 0.2, 0.3)
  • 十六进制#0f0f0f
  • 十六进制:用 #abc as #aabbcc
  • 字符串代替浮点数:0 as black; 1 as white; 0.8 as light blue.
  • 单字符串作为基本颜色:b as blue; g as green; r as red; c as cyan; m as magenta; y as yellow; k as black; w as white.
  • X11/CSS4aquamarine
  • xkcdxkcd:sky blue
  • Tableau 10: tab: blue; tab: orange
  • CN:C0,默认有个颜色盘,C0 指的是其第 1 个颜色

Colormap

Third-party colormaps

Colorbar

Colorbar 是数据标量值到颜色的映射,比如在热度图中就需要 Colorbar 对数据值进行展示。将已有的一个 Axes 对象传入给 colorbar 方法,则可自动根据 Axes 的数据生成 colorbar, 先感性地感受下什么是 colorbar:

# import matplotlib.pyplot as plt
# import matplotlib as mpl

fig = plt.figure(figsize=(6, 1))
ax1 = fig.add_subplot(111)
fig.subplots_adjust(bottom=0.5)  # 设置子图到下边界的距离

cmap = mpl.cm.spring
norm = mpl.colors.Normalize(vmin=5, vmax=10)

fig.colorbar(
    mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
    cax=ax1,  # 传入的是一个子图对象
    orientation="horizontal",
    label="spring",
)

plt.show()

Fig. 15: Colorbar

调节 colorbar 两边样式:

fig, ax = plt.subplots(4, 1, figsize=(6, 4), constrained_layout=True)

cmap = mpl.cm.viridis
bounds = [-1, 2, 5, 7, 12, 15]

for i, extend in (0, "neither"), (1, "min"), (2, "max"), (3, "both"):
    norm = mpl.colors.BoundaryNorm(bounds, cmap.N, extend=extend)
    fig.colorbar(
        mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
        cax=ax[i],
        extendfrac="auto",
        orientation="horizontal",
        label=f"extend={extend}",
    )

plt.show()

Fig. 16: Colorbar-extension

下面介绍下 figurecolorbar 方法

10 Tricks

10.1 Bbox_inches and pad_inches

图形有边界框,bbox_inches 就是控制该边框的参数,如果将其设置为 tight,即可刚好把子图所有元素包围。注意,该命令可能会覆盖 subplots_adjust 的设置。

但这并不能得到一张四周无空白的图形,因为还有个 pad_inches 参数在控制上步骤得到的内边框到整个画布外边框的距离,默认值是 \(0.1\),将其设置为 \(0\) 即可得到一个四边无边框的最终图形:

fig = plt.figure(
    figsize=(8, 6), facecolor="pink", frameon=True, edgecolor="green", linewidth=2
)

ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")

x = np.linspace(0, 10, 100)

ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))

# fig.savefig(
#     fname='Gaussian distribution.pdf',
#     bbox_inches='tight',  # 尝试找出一个边框,使其刚好能框出子图部分所有元素(包括坐标轴标签等),一般设置为 tight 即可。需要注意的是该命令只能用于保存图片,不能用于显示。
#     pad_inches=0)  # 所保存图形周围的填充量,默认是 0.1

plt.show()

Fig. 17: Bbox_inches and pad_inches

Warning

pad_inches=0 表示的是右边和上边完全无空白,只能在 savefig() 命令下正确显示。 实际使用中,在 Fig. 17 添加fig.savefig()命令。

还需要注意的一个细节是,虽然pad_inches=0 可以使右边和上边完全无空白,但因为锚点缘故,会裁掉一部分边框,如果希望完全无空白且边框不被裁,那边需要更深入地了解 anchor 这一元素,若觉得麻烦,则可以给其一个很小的数值即可,比如pad_inches=0.01

10.2 Tight_layout

根据官网文档描述,tight_layout() 使坐标轴长度成为了弹性体,根据坐标轴标签、刻度线标签是否重叠重新分配其与坐标轴的空间,以确保其能完全显示。具体而言,它调节的是:

  • 子图间的横向和纵向间距(h_pad=1 ,w_pad=1),单位是默认字体的比例
  • 整个子图区域和画布的间距(pad=0rect=(0, 0, 1, 1)

从上面的描述可以看出,与 bbox_inches()pad_inches() 相比,tight_layout() 增加了对子图之间更细节的设置:

fig = plt.figure(
    figsize=(8, 6),
    facecolor="pink",
    frameon=True,
    edgecolor="green",
    tight_layout=dict(
        h_pad=1, w_pad=1, rect=(0, 0, 1, 1)  # 单位为 fontsize 大小
    ),  # 包含子图的所有元素,比如坐标轴标签等
    linewidth=2,
)

ax1 = fig.add_subplot(221, xlabel="$x$", ylabel="$y$")
ax2 = fig.add_subplot(222, xlabel="$x$", ylabel="$y$")
ax3 = fig.add_subplot(223, xlabel="$x$", ylabel="$y$")
ax4 = fig.add_subplot(224, xlabel="$x$", ylabel="$y$")

x = np.linspace(0, 10, 100)

ax1.plot(np.sin(x))
ax2.plot(np.cos(x))
ax3.plot(np.sin(x))
ax4.plot(np.cos(x))
plt.show()
# fig.savefig('Gaussian distribution.pdf', bbox_inches='tight', pad_inches=0)

Fig. 18: Tight_layout

对比 Fig. 18Fig. 6 ,可以清楚地看出重叠部分已被重新调整。当然了,你也可以选择像 Fig. 7 一样调整。但明显 tight_layout() 更加简洁。

Warning

pad_inches=0 表示的是右边和上边完全无空白,只能在 savefig() 命令下正确显示。 实际使用中,在 Fig. 18 添加fig.savefig()命令。

在实际使用中,我也最推荐这种 tight_layout()bbox_inchespad_inches 结合的方法,而不太推荐 subplots_adjust()

至此,我们几乎可以将子图排列整齐,且完美显示出来。但注意,我们至此并没有涉及到任何 anchor 的内容,如果精细地控制图形的每一点设置,anchor 是一个绕不开的内容。但无论如何,我认为到此为止已经可以得到一张接近完美的画布了。

11 Default setting

在 matplotlib 中,运行如下代码即可获取全部可自定义参数:

import matplotlib as mpl

print(mpl.rc_params()) 

默认参数的设置格式如下:

import matplotlib as mpl

mpl.rcParams['figure.figsize'] = (8, 6) # 将画布大小设置为 8*6

一共有 304 个参数可设置,下面仅列举常用参数。

11.1 figure

figure.dpi: 100.0 # 图像dpi,但是我一般喜欢直接输出 pdf
figure.figsize: [6.4, 4.8] # 画布大小
figure.frameon: True # 显示画布边框
figure.edgecolor: white # 边框颜色
figure.facecolor: white # 画布背景
figure.titlesize: large # 标题字体大小
figure.titleweight: normal # 标题字重
figure.raise_window: True # 弹出图形窗口
figure.autolayout: False
figure.constrained_layout.h_pad: 0.04167
figure.constrained_layout.hspace: 0.02
figure.constrained_layout.use: False
figure.constrained_layout.w_pad: 0.04167
figure.constrained_layout.wspace: 0.02
figure.subplot.left: 0.125
figure.subplot.right: 0.9
figure.subplot.top: 0.88
figure.subplot.bottom: 0.11
figure.subplot.hspace: 0.2 # 子图间高度
figure.subplot.wspace: 0.2 # 子图间宽度

11.2 axes

axes.edgecolor: black # 图形边框
axes.facecolor: white # 图形背景
axes.grid: False # 网格
axes.grid.axis: both # 网格方向
axes.grid.which: major # 网格位置:major, minor
axes.labelcolor: black # 标签颜色
axes.labelpad: 4.0 # 标签离轴偏离
axes.labelsize: medium # 标签大小
axes.labelweight: normal # 标签字重
axes.linewidth: 0.8 # 轴线宽
axes.spines.bottom: True # 显示下轴线
axes.spines.left: True # 显示左轴线
axes.spines.right: True # 显示右轴线
axes.spines.top: True # # 显示上轴线
axes.titlecolor: auto # 图形标题颜色
axes.titlelocation: center # 图形标题位置
axes.titlepad: 6.0 # 图形标题偏离
axes.titlesize: large # 图形标题大小
axes.titleweight: normal # 图形标题字重
axes.xmargin: 0.05 #坐标轴根据 x 数据值左边扩展 0.05
axes.ymargin: 0.05
axes.zmargin: 0.05

11.3 xaxis and xtick

xaxis.labellocation: center # 标签位置
xtick.alignment: center # 对齐
xtick.bottom: True # 刻度线位置
xtick.color: black # 颜色
xtick.direction: out # 方向
xtick.labelbottom: True # 标签位置
xtick.labelcolor: inherit # 标签颜色
xtick.labelsize: medium # 标签字重
xtick.labeltop: False # 标签位置
xtick.major.bottom: True # 主刻度线位置
xtick.major.pad: 3.5 # 主刻度线偏离
xtick.major.size: 3.5 # 主刻度线长度
xtick.major.top: True 
xtick.major.width: 0.8 # 主刻度线宽度
xtick.minor.bottom: True
xtick.minor.pad: 3.4
xtick.minor.size: 2.0
xtick.minor.top: True
xtick.minor.visible: False # 显示副刻度线
xtick.minor.width: 0.6
xtick.top: False

11.4 yaxis and ytick

yaxis.labellocation: center
ytick.alignment: center_baseline
ytick.color: black
ytick.direction: out
ytick.labelcolor: inherit
ytick.labelleft: True
ytick.labelright: False
ytick.labelsize: medium
ytick.left: True
ytick.major.left: True
ytick.major.pad: 3.5
ytick.major.right: True
ytick.major.size: 3.5
ytick.major.width: 0.8
ytick.minor.left: True
ytick.minor.pad: 3.4
ytick.minor.right: True
ytick.minor.size: 2.0
ytick.minor.visible: False
ytick.minor.width: 0.6
ytick.right: False

11.5 font

font.family: ['sans-serif'] # 使用衬线字体还是非衬线字体
font.sans-serif: ['DejaVu Sans', 'Bitstream Vera Sans', 'Computer Modern Sans Serif', 'Lucida Grande', 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica', 'Avant Garde', 'sans-serif'] # 非衬线字体集
font.serif: ['DejaVu Serif', 'Bitstream Vera Serif', 'Computer Modern Roman', 'New Century Schoolbook', 'Century Schoolbook L', 'Utopia', 'ITC Bookman', 'Bookman', 'Nimbus Roman No9 L', 'Times New Roman', 'Times', 'Palatino', 'Charter', 'serif'] # 非衬线字体集
font.monospace: ['DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Computer Modern Typewriter', 'Andale Mono', 'Nimbus Mono L', 'Courier New', 'Courier', 'Fixed', 'Terminal', 'monospace'] # 等宽字体集
font.size: 10.0 # 字体大小
font.style: normal # 字体风格:normal, italic, oblique
font.weight: normal # 字重:normal, bold, lighterm bolder

11.6 grid

grid.alpha: 1.0 # 透明度
grid.color: #b0b0b0 # 颜色
grid.linestyle: - # 线样式
grid.linewidth: 0.8 # 线宽

11.7 legend

legend.loc: best # 图例位置
legend.frameon: True # 显示边框
legend.framealpha: 0.8 # 边框透明度
legend.edgecolor: 0.8 # 边框颜色
legend.facecolor: inherit # 背景颜色
legend.title_fontsize: None # 图例标题的字体大小
legend.borderpad: 0.4 # 边框偏离
legend.columnspacing: 2.0 # 列间隔
legend.fontsize: medium # 图例字体大小
legend.labelcolor: None # 图例标签颜色
legend.labelspacing: 0.5 # 图例和标签间隔
legend.fancybox: True # 边框阴影

11.8 savefig

savefig.bbox: None
savefig.dpi: figure # dpi
savefig.format: png # 格式
savefig.orientation: portrait # 方向
savefig.transparent: False # 是否透明
savefig.edgecolor: auto # 边框颜色 
savefig.facecolor: auto # 背景色
savefig.directory: ~ # 保存路径

11.9 scatter

scatter.edgecolors: face # 散点图边框颜色
scatter.marker: o # 散点图标记

11.10 lines

lines.linestyle: - # 样式
lines.linewidth: 1.5 # 宽度
lines.color: C0 # 颜色
lines.dashdot_pattern: [6.4, 1.6, 1.0, 1.6] # dashdot 样式
lines.dashed_pattern: [3.7, 1.6] # dashed 样式
lines.dotted_pattern: [1.0, 1.65] # dotted 样式
lines.marker: None
lines.markersize: 6.0 # 线上样本点大小
lines.markeredgecolor: auto # 线上样本点边缘颜色
lines.markeredgewidth: 1.0 # 线上样本点边框宽度
lines.markerfacecolor: auto # 线上样本点背景色
lines.scale_dashes: True 

11.11 text

text.color: black # 字体颜色
text.usetex: False # 是否使用 latex
text.latex.preamble:  # 字体 latex 导言区
text.hinting: force_autohint # 字体微调

11.12 mathtext

mathtext.default: it # 默认字体样式
mathtext.fontset: dejavusans # 字体种类
mathtext.it: sans:italic
mathtext.rm: sans
mathtext.sf: sans
mathtext.tt: monospace
mathtext.bf: sans:bold # 粗体
mathtext.cal: cursive # 计算符号,草书
mathtext.fallback: cm

11.13 others

``yml markers.fillstyle: full # 填充样式